package edu.ilstu.mind.ise.module.arduino.controller;

import java.io.IOException;

import edu.ilstu.mind.ise.module.arduino.serial.SerialCommunicator;

/**
 * Implementation of the Arduino Controller that uses a
 * {@link SerialCommunicator} to communicate with the Arduino board
 * 
 * @author Eric Thill
 * 
 */
public class SerialArduinoController implements ArduinoController {

	public static int ANALOG_WRITE_MIN = 0;
	public static int ANALOG_WRITE_MAX = 255;
	public static int ANALOG_READ_MIN = 0;
	public static int ANALOG_READ_MAX = 1023;

	private static final char PIN_TYPE_DIGITAL_KEY = 'D';
	private static final char PIN_TYPE_ANALOG_KEY = 'A';
	private static final byte PIN_TYPE_DIGITAL_BYTEVALUE = 0;
	private static final byte PIN_TYPE_ANALOG_BYTEVALUE = 1;

	private static final byte HEADER_PING = 0;
	private static final byte HEADER_GET_VERSION = 1;
	private static final byte HEADER_SET_PINMODE = 2;
	private static final byte HEADER_SET_DIGITAL_OUT = 3;
	private static final byte HEADER_SET_ANALOG_OUT = 4;
	private static final byte HEADER_GET_DIGITAL_IN = 5;
	private static final byte HEADER_GET_ANALOG_IN = 6;

	private final SerialCommunicator serialCommunicator;

	public SerialArduinoController(SerialCommunicator serialCommunicator) {
		this.serialCommunicator = serialCommunicator;
	}

	@Override
	public boolean ping() {
		try {
			// lock for thread safety
			serialCommunicator.lock();
			// write command
			serialCommunicator.write(HEADER_PING);
			// no exception by now => success
			return true;
		} catch (IOException e) {
			return false;
		} finally {
			// always unlock regardless of errors
			serialCommunicator.unlock();
		}
	}

	@Override
	public String getVersion() throws ArduinoException {
		try {
			// lock for thread safety
			serialCommunicator.lock();
			// write command
			serialCommunicator.write(HEADER_GET_VERSION);
			// check if board will return the value or if there was an error
			boolean success = serialCommunicator.read() != 0;
			if (!success) {
				throw new ArduinoException("Arduino Board does not recognize version command");
			}
			// construct version string byte by byte
			StringBuilder version = new StringBuilder();
			byte b;
			while ((b = serialCommunicator.read()) != '\0') {
				version.append((char) b);
			}
			// return the version string
			return version.toString();
		} catch (IOException e) {
			throw new ArduinoException(e);
		} finally {
			// always unlock regardless of errors
			serialCommunicator.unlock();
		}
	}

	@Override
	public void setPinMode(String pinId, PinMode pinMode) throws ArduinoException {
		// parse pin type and number
		byte pinType = parsePinType(pinId);
		int pinNum = parsePinNum(pinId);
		try {
			// lock for thread safety
			serialCommunicator.lock();
			// write command
			serialCommunicator.write(HEADER_SET_PINMODE);
			// write pin command is for
			writePin(serialCommunicator, pinType, pinNum);
			// write pin mode as byte value
			serialCommunicator.write(pinMode.getByteValue());
			// check if board is reporting an error
			boolean success = serialCommunicator.read() != 0;
			if (!success) {
				throw new ArduinoException("Arduino Board encounted exception setting pin mode <" + pinId + "=" + pinMode + ">");
			}
		} catch (IOException e) {
			throw new ArduinoException(e);
		} finally {
			// always unlock regardless of errors
			serialCommunicator.unlock();
		}
	}

	@Override
	public void digitalWrite(String pinId, boolean value) throws ArduinoException {
		// parse pin type and number
		byte pinType = parsePinType(pinId);
		int pinNum = parsePinNum(pinId);
		byte byteValue = value ? (byte) 1 : 0;
		try {
			// lock for thread safety
			serialCommunicator.lock();
			// write command
			serialCommunicator.write(HEADER_SET_DIGITAL_OUT);
			// write pin command is for
			writePin(serialCommunicator, pinType, pinNum);
			// write value as byte value
			serialCommunicator.write(byteValue);
			// check if board is reporting an error
			boolean success = serialCommunicator.read() != 0;
			if (!success) {
				throw new ArduinoException("Arduino Board encounted exception writing DIGITAL_OUTPUT <" + pinId + "=" + value + ">");
			}
		} catch (IOException e) {
			throw new ArduinoException(e);
		} finally {
			// always unlock regardless of errors
			serialCommunicator.unlock();
		}
	}

	@Override
	public void analogWrite(String pinId, int value) throws ArduinoException {
		// parse pin type and number
		byte pinType = parsePinType(pinId);
		int pinNum = parsePinNum(pinId);
		// check to make sure value is in proper range
		if (value < ANALOG_WRITE_MIN || value > ANALOG_WRITE_MAX) {
			throw new ArduinoException(pinId + "=" + value + " is out of valid range");
		}
		try {
			// lock for thread safety
			serialCommunicator.lock();
			// write command
			serialCommunicator.write(HEADER_SET_ANALOG_OUT);
			// write pin command is for
			writePin(serialCommunicator, pinType, pinNum);
			// write value as byte value
			serialCommunicator.write((byte) value);
			// check if board is reporting an error
			boolean success = serialCommunicator.read() != 0;
			if (!success) {
				throw new ArduinoException("Arduino Board encounted exception writing ANALOG_OUTPUT <" + pinId + "=" + value + ">");
			}
		} catch (IOException e) {
			throw new ArduinoException(e);
		} finally {
			// always unlock regardless of errors
			serialCommunicator.unlock();
		}
	}

	@Override
	public boolean digitalRead(String pinId) throws ArduinoException {
		// parse pin type and number
		byte pinType = parsePinType(pinId);
		int pinNum = parsePinNum(pinId);
		try {
			// lock for thread safety
			serialCommunicator.lock();
			// write command
			serialCommunicator.write(HEADER_GET_DIGITAL_IN);
			// write pin command is for
			writePin(serialCommunicator, pinType, pinNum);
			// check if board will return the value or if there was an error
			boolean success = serialCommunicator.read() != 0;
			if (!success) {
				throw new ArduinoException("Arduino Board encounted exception reading DIGITAL_INPUT <" + pinId + ">");
			}
			// read the value from the board
			boolean value = serialCommunicator.read() != 0;
			// return the boolean value
			return value;
		} catch (IOException e) {
			throw new ArduinoException(e);
		} finally {
			// always unlock regardless of errors
			serialCommunicator.unlock();
		}
	}

	@Override
	public int analogRead(String pinId) throws ArduinoException {
		// parse pin type and number
		byte pinType = parsePinType(pinId);
		int pinNum = parsePinNum(pinId);
		try {
			// lock for thread safety
			serialCommunicator.lock();
			// write command
			serialCommunicator.write(HEADER_GET_ANALOG_IN);
			// write pin command is for
			writePin(serialCommunicator, pinType, pinNum);
			// check if board will return the value or if there was an error
			boolean success = serialCommunicator.read() != 0;
			if (!success) {
				throw new ArduinoException("Arduino Board encounted exception reading ANALOG_INPUT <" + pinId + ">");
			}
			// the value is a short, so read the 2 bytes from the board
			byte b1 = serialCommunicator.read();
			byte b2 = serialCommunicator.read();
			// convert the 2 bytes to a single number
			int value = (b1 << 8) + b2;
			// check to make sure value read is in proper range. Don't just
			// return a garbage value out of range.
			if (value < ANALOG_READ_MIN || value > ANALOG_READ_MAX) {
				throw new ArduinoException("Arduino Board returned an erroneous value when reading ANALOG_INPUT <" + pinId + ">");
			}
			// return the int value
			return value;
		} catch (IOException e) {
			throw new ArduinoException(e);
		} finally {
			// always unlock regardless of errors
			serialCommunicator.unlock();
		}
	}

	private static byte parsePinType(String pinId) throws ArduinoException {
		// make sure there is a value in the string
		if (pinId == null || pinId.length() < 1) {
			throw new ArduinoException("Cannot have a pinId of length 0");
		}
		// first character is the pin type
		char c = pinId.charAt(0);
		// convert first character from the char to the byte value recognized by
		// the board
		if (c == PIN_TYPE_DIGITAL_KEY) {
			// 'D' pin
			return PIN_TYPE_DIGITAL_BYTEVALUE;
		} else if (c == PIN_TYPE_ANALOG_KEY) {
			// 'A' pin
			return PIN_TYPE_ANALOG_BYTEVALUE;
		} else {
			// not a valid pin type
			throw new ArduinoException(pinId + " is not a valid pin");
		}
	}

	private static int parsePinNum(String pinId) throws ArduinoException {
		// make sure there is a value in the string
		if (pinId == null || pinId.length() < 2) {
			throw new ArduinoException("Cannot have a pinId without the pin number");
		}
		try {
			// parse the pin number part of the pinId
			return Integer.parseInt(pinId.substring(1));
		} catch (NumberFormatException e) {
			// not a valid pin number
			throw new ArduinoException(pinId + " is not a valid pin");
		}
	}

	private static void writePin(SerialCommunicator serialCommunicator, byte pinType, int pinNum) throws IOException {
		// write pin type
		serialCommunicator.write(pinType);
		// write 4 bytes that make up the pin number
		serialCommunicator.write((byte) ((pinNum >>> 24) & 0xff));
		serialCommunicator.write((byte) ((pinNum >>> 16) & 0xff));
		serialCommunicator.write((byte) ((pinNum >>> 8) & 0xff));
		serialCommunicator.write((byte) (pinNum & 0xff));
	}
}